/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import { ArkMethod, ArkFile, ArkClass } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from "../../model/Defects";
import { ClassMatcher, FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";

const defaultOptions: Options = {
    ignoreDifferentlyNamedParameters: false
}

interface Options {
    /** Whether two parameters with different names at the same index should be considered different even if their types are the same. */
    ignoreDifferentlyNamedParameters?: boolean;
}

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'UnifiedSignaturesCheck');
const gmetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/unified-signatures-check.md", // TODO: support url
    description: "Disallow two overloads that could be unified into one with a union or an optional/rest parameter."
};

export class UnifiedSignaturesCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gmetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };
    private classMatcher: ClassMatcher = {
        file: [this.fileMatcher],
        matcherType: MatcherTypes.CLASS
    };

    public registerMatchers(): MatcherCallback[] {
        const matchClass: MatcherCallback = {
            matcher: this.classMatcher,
            callback: this.check
        }
        return [matchClass];
    }

    public check = (Class: ArkClass): void => {
        // 将规则选项转换为Options类型
        let options: Options;
        if (this.rule && this.rule.option.length > 0) {
            options = this.rule.option[0] as Options;
        } else {
            options = defaultOptions;
        }
        let methodNameArray: string[] = [];
        let parameterTypeNameArray: string[] = [];
        let parameterNameArray: string[] = [];
        const methods = Class.getMethods();
        for (const method of methods) {
            let declareClass = method.getDeclaringArkClass();
            //获取当前方法的声明文件;
            const arkFile = declareClass.getDeclaringArkFile();
            const methodSignatures = method.getDeclareSignatures();
            if (methodSignatures) {
                if (methodSignatures.length === 1) {
                    continue;
                }
                for (const methodSignature of methodSignatures) {
                    this.extractMethodDetails(methodSignature, methodNameArray, parameterTypeNameArray, parameterNameArray);
                }
                if (options.ignoreDifferentlyNamedParameters === false) {
                    this.handleMethodSignatures(arkFile, method, methodSignatures, methodNameArray, parameterTypeNameArray, parameterNameArray);
                }
                if (options.ignoreDifferentlyNamedParameters === true) {
                    this.checkMethodSignatures(arkFile, method, methodSignatures, methodNameArray, parameterTypeNameArray, parameterNameArray);
                }
            }
        }
    }

    private handleMethodSignatures(
        arkFile: ArkFile,
        method: ArkMethod,
        methodSignatures: any[],
        methodNameArray: string[],
        parameterTypeNameArray: string[],
        parameterNameArray: string[]
    ): void {
        for (let i = 0; i < methodNameArray.length - 1; i++) {
            if (methodNameArray[i + 1] !== methodNameArray[0]) {
                continue;
            }
            if (parameterTypeNameArray.length > 1) {
                for (let j = i + 1; j < parameterTypeNameArray.length; j++) {
                    this.addIssueReport(arkFile, method, parameterTypeNameArray[j], parameterNameArray, j);
                }
                parameterTypeNameArray = [];
            } else if (parameterTypeNameArray.length === 1) {
                this.processMethodSignatures(methodSignatures, arkFile, method, parameterTypeNameArray, parameterNameArray, i);
            }
        }
    }

    private processMethodSignatures(
        methodSignatures: any[],
        arkFile: ArkFile,
        method: ArkMethod,
        parameterTypeNameArray: string[],
        parameterNameArray: string[],
        i: number
    ): void {
        for (const methodSignature of methodSignatures) {
            const methodSubSignature = methodSignature.getMethodSubSignature();
            const parameters = methodSubSignature.getParameters();
            if (parameters.length === 0) {
                continue;
            }
            for (const parameter of parameters) {
                if (!parameter.hasDotDotDotToken()) {
                    continue;
                }
                this.handleDotDotDotParameters(parameters, arkFile, method, parameterTypeNameArray, parameterNameArray, i, true);
            }
        }
    }

    private handleDotDotDotParameters(
        parameters: any[],
        arkFile: ArkFile,
        method: ArkMethod,
        parameterTypeNameArray: string[],
        parameterNameArray: string[],
        i: number,
        dotdotdot: boolean
    ): void {
        for (let j = i + 1; j <= parameterTypeNameArray.length; j++) {
            const parameterTypeName = parameterTypeNameArray.length > 1 ? parameterTypeNameArray[j] : parameterTypeNameArray[i];
            this.addIssueReport(arkFile, method, parameterTypeName, parameterNameArray, j, dotdotdot);
        }
    }

    private checkMethodSignatures(
        arkFile: ArkFile,
        method: ArkMethod,
        methodSignatures: any[],
        methodNameArray: string[],
        parameterTypeNameArray: string[],
        parameterNameArray: string[]
    ): void {
        for (let i = 0; i < methodNameArray.length - 1; i++) {
            if (methodNameArray[i + 1] !== methodNameArray[0]) {
                continue;
            }
            if (parameterTypeNameArray.length > 1) {
                this.checkParameterNames(arkFile, method, parameterTypeNameArray, parameterNameArray, i, () => {
                    parameterTypeNameArray = [];
                });
            } else if (parameterTypeNameArray.length === 1) {
                this.processMethodSignaturesWithDotDotDot(methodSignatures, arkFile, method, parameterTypeNameArray, parameterNameArray, i);
            }
        }
    }


    private extractMethodDetails(
        methodSignature: any,
        methodNameArray: string[],
        parameterTypeNameArray: string[],
        parameterNameArray: string[]
    ): void {
        const methodSubSignature = methodSignature.getMethodSubSignature();
        const methodName = methodSubSignature.getMethodName();
        methodNameArray.push(methodName);
        const parameters = methodSubSignature.getParameters();
        if (parameters.length > 0) {
            for (const parameter of parameters) {
                const parameterType = parameter.getType();
                const parameterName = parameter.getName();
                const parameterTypeName = parameterType?.toString() ?? '';
                parameterTypeNameArray.push(parameterTypeName);
                parameterNameArray.push(parameterName);
            }
        }
    }

    private checkParameterNames(
        arkFile: ArkFile,
        method: ArkMethod,
        parameterTypeNameArray: string[],
        parameterNameArray: string[],
        i: number,
        callback: () => void
    ): void {
        for (let j = i + 1; j < parameterTypeNameArray.length; j++) {
            const parameterTypeName = parameterTypeNameArray[j];
            for (let l = 0; l < parameterNameArray.length - 1; l++) {
                this.shouldAddIssueReport(parameterNameArray, l, parameterTypeName, arkFile, method, j);
            }
        }
        callback();
    }

    private shouldAddIssueReport(
        parameterNameArray: string[],
        l: number,
        parameterTypeName: string,
        arkFile: ArkFile,
        method: ArkMethod,
        j: number
    ): void {
        if (parameterNameArray.length <= 2) {
            if (parameterNameArray[l + 1] === parameterNameArray[0]) {
                this.addIssueReport(arkFile, method, parameterTypeName, parameterNameArray, j);
            }
        } else if (parameterNameArray.length > 2) {
            if (parameterNameArray[l + 3] === parameterNameArray[2]) {
                this.addIssueReport(arkFile, method, parameterTypeName, parameterNameArray, j);
            }
        }
    }

    private processMethodSignaturesWithDotDotDot(
        methodSignatures: any[],
        arkFile: ArkFile,
        method: ArkMethod,
        parameterTypeNameArray: string[],
        parameterNameArray: string[],
        i: number
    ): void {
        for (const methodSignature of methodSignatures) {
            const methodSubSignature = methodSignature.getMethodSubSignature();
            const parameters = methodSubSignature.getParameters();
            if (parameters.length === 0) {
                continue;
            }
            for (const parameter of parameters) {
                if (!parameter.hasDotDotDotToken()) {
                    continue;
                }
                const parameterTypeName = parameterTypeNameArray.length > 1 ? parameterTypeNameArray[i + 1] : parameterTypeNameArray[i];
                for (let j = i + 1; j <= parameterTypeNameArray.length; j++) {
                    this.addIssueReport(arkFile, method, parameterTypeName, parameterNameArray, j, true);
                }
            }
        }
    }

    private addIssueReport(arkFile: ArkFile, method: ArkMethod, typeName: string,
        parameter: string[], colNum: number, dotdotdot?: boolean): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(arkFile, method, typeName, parameter, colNum, dotdotdot);
        if (warnInfo) {
            this.metaData.description = this.metaData.description;
            const filePath = arkFile.getFilePath();
            let defects = new Defects(warnInfo.line, warnInfo.startCol,
                warnInfo.endCol, this.metaData.description, severity, this.rule.ruleId, filePath,
                this.metaData.ruleDocPath, true, false, false);
            this.issues.push(new IssueReport(defects, undefined));
            RuleListUtil.push(defects);
        } else {
            logger.debug('No valid line number found for issue report.');
        }
    }

    private getLineAndColumn(arkfile: ArkFile, method: ArkMethod, typeName: string,
        parameter: string[], colNum: number, dotdotdot?: boolean): {
            line: number, startCol: number, endCol: number, filePath: string
        } | null {
        if (!arkfile) {
            logger.debug('originStmt or arkFile is null');
            return null;
        }
        const code = arkfile.getCode() ?? '';
        const lines = code.split('\r\n');
        const methodLineCols = method.getDeclareLines();
        const lineNumber = methodLineCols && colNum < methodLineCols.length ? methodLineCols[colNum] : undefined;
        for (const line of lines) {
            const result = this.getLineAndColumn1(line, typeName, parameter, arkfile, dotdotdot, lineNumber);
            if (result) {
                return result;
            }
        }
        return null;
    }
    private getLineAndColumn1(line: string, typeName: string, parameter: string[], arkfile: ArkFile, dotdotdot?: boolean,
        lineNumber?: number): { line: number; startCol: number; endCol: number; filePath: string } | null {
        const lineIndex = line.indexOf(typeName);
        if (lineIndex !== -1) {
            const indexInfo = this.getParameterIndexAndColumn(line, parameter, dotdotdot ?? false);
            if (indexInfo && lineNumber !== undefined) {
                const originPath = arkfile.getFilePath();
                return { line: lineNumber, startCol: indexInfo.startCol, endCol: indexInfo.endCol, filePath: originPath };
            }
        }
        return null;
    }
    private getParameterIndexAndColumn(line: string, parameter: string[], dotdotdot: boolean): { startCol: number, endCol: number } | null {
        let modifiedParameter: string;
        if (dotdotdot) {
            modifiedParameter = '...' + parameter[0];
        } else {
            modifiedParameter = '(' + parameter[1];
        }
        const lineIndex = line.indexOf(modifiedParameter);
        if (lineIndex !== -1) {
            let startCol;
            if (dotdotdot) {
                startCol = lineIndex + 1;
            } else {
                startCol = lineIndex + 2;
            }
            const endCol = lineIndex + modifiedParameter.length;
            return { startCol, endCol };
        }
        return null;
    }
}